iT邦幫忙

1

【Petite-Vue】大小只有 ~6KB 的 mini Vue!

  • 分享至 

  • xImage
  •  


文章出處


Vue 可以作為獨立的腳本文件使用,無需構建步驟!如果你有一個非前後端分離的歷史項目,並且它已經渲染了大部分的 HTML,或者你的前端邏輯並不復雜,不需要構建步驟,Vue 也提供了另一個適用於此類無構建步驟場景的替代版 Petite-Vue,主要為漸進式增強已有的 HTML 作了特別的優化。功能更加精簡,十分輕量。


Petite-Vue 的特點

相對 Vue3 而言,Petite-Vue 有如下特點:

  1. 提供精簡版的與 Vue3 語法和表現一致的模板語言
  2. 僅由渲染模塊和響應式系統模塊組成
  3. 渲染模塊沒有採用虛擬 DOM,而是採用在線解析渲染的方式
  4. 響應式系統模塊對外暴露 reactive 接口提供構建全局狀態管理器的能力
  5. 代碼庫體積在 gzip 壓縮後 ~6KB,十分適合與項目已有的 LayUI、MiniUI 等 UI 庫搭配使用

什麼是 Petite-Vue?

根據官方解釋,Petite-Vue 是專門為非前後端分離的歷史項目提供和 Vue 相近的響應式開發模式。與完整的 Vue 相比最大的特點是,面對數據的變化 Petite-Vue 採取直接操作 DOM 的方式重新渲染。

具體的使用方式請參考 GitHub,在這裡我想展示兩個示例:

代碼庫結構介紹

  • examples 各種使用示例
  • scripts 打包發布腳本
  • tests 測試用例
  • src
    • directives v-if 等內置指令的實現
    • app.ts createApp 函數
    • block.ts 塊對象
    • context.ts 上下文對象
    • eval.ts 提供 v-if="count === 1" 等表達式運算功能
    • scheduler.ts 調度器
    • utils.ts 工具函數
    • walk.ts 模板解析

若想構建自己的版本只需在控制台執行 npm run build 即可。

範例(一) - 在線渲染

<style>
  [v-cloak] {
    display: none;
  }
</style>
<div v-scope="App"></div>

<script type="module">
  import { createApp } from 'https://unpkg.com/petite-vue?module'

  createApp({
    App: {
      $template: `
      <span v-cloak v-if="status === 'offline'"> OFFLINE </span>
      <span v-else> ONLINE </span>
      `,
    }
    status: 'online'
  }).mount('[v-scope]')
</script>

上述代碼最終輸出結果為 <div><span> ONLINE </span></div>,但渲染過程是直接在 DOM Tree 上進行(分為如下4個步驟),當瀏覽器資源緊張時整個渲染過程將會被用戶一覽無餘:

  1. 生成模板
<div>
  <span v-cloak v-if="status === 'offline'"> OFFLINE </span>
  <span v-else> ONLINE </span>
</div>
  1. 移除 v-cloak 屬性
<div>
  <span v-if="status === 'offline'"> OFFLINE </span>
  <span v-else> ONLINE </span>
</div>
  1. 解析 v-if 和 v-else 指令
<div>
  <span v-if="status === 'offline'"> OFFLINE </span>
</div>
<div>
</div>
  1. 最終渲染
<div>
  <span> ONLINE </span>
</div>

那麼用戶很有可能會看到閃屏現象:ONLINE ➡️ OFFLINE ONLINE ➡️ OFFLINE ➡️ EMPTY ➡️ ONLINE

範例(二) - 組件化

<div v-scope="App"></div>

<script type="module">
  import { createApp } from 'https://unpkg.com/petite-vue?module'

  const App = {
    $template: `
      <div v-scope="Counter(1)"></div>
      <div v-scope="Message()"></div>
    `
  }

  const Counter = initialCount => ({
    $template: `
      <span></span>
      <button @click="handleAdd">ADD</button>
    `,
    count: initialCount || 0
    handleAdd() {
      this.count += 1
    }
  })

  const Message = () => {
    $template: `<div></div>`
  }

  createApp({
    App,
    Counter,
    Message,
  }).mount('[v-scope]')
</script>

Petite-Vue 雖然沒有提供明確的組件構建方式,但通過 v-scope 屬性我們依然可以採取組件化構建我們的頁面。

但上述例子有明顯的問題採取全局組件註冊機制,如例子中即使 Message 組件不需要還是能引用 Counter 組件,假如註冊的不是 Counter 組件的構造函數,那麼 Counter 的狀態將會被意外修改。

createApp({
  Counter: Counter()
})

快速上手

自動初始化

Petite-Vue 無需構建流程即可使用。只需從 CDN 加載它:

<script src="https://unpkg.com/petite-vue" defer init></script>

<!-- 頁面任意位置 --> 
<div v-scope="{ count: 0 }">
  {{ count }}
  <button @click="count++">inc</button>
</div>
  1. 用 v-scope 標記頁面上應由 Petite-Vue 控制的區域,同時也是聲明 data 與 methods。
  2. 該 defer 屬性使腳本在 HTML 內容被解析後執行。
  3. 該 init 屬性告訴 Petite-Vue 自動查找和初始化頁面上的所有包含 v-scope 的元素。
  • 這裡的 init 其實也就是 PetiteVue.createApp().mount() 的簡潔寫法;
  • 閱讀源碼可知:
    • const s = document.currentScript;
    • 如果 ( s && s . hasAttribute ( 'init ' )) {
    • createApp().mount();
    • }
  • 如果不想自動初始化,那麼移除 init 屬性,且在 script 標籤中,增加 PetiteVue.createApp().mount()

手動初始化

如果您不想要自動初始化,請刪除該 init 屬性並將腳本移動到 <body> 末尾:

<script src="https://unpkg.com/petite-vue"></script>
<script>
  PetiteVue.createApp().mount()
</script>

或者,使用 ES 模塊構建:

<script type="module">
  import { createApp } from 'https://unpkg.com/petite-vue?module'
  createApp().mount()
</script>

重點:在開發時期,CDN 地址可以是 https://unpkg.com/petite-vue 這樣簡短的,但對於生產使用時,應該要使用完整解析的 CDN URL。
如:https://unpkg.com/petite-vue@0.4.1/dist/petite-vue.iife.js 或者 https://unpkg.com/petite-vue@0.4.1/dist/petite-vue.es.js (ES 模塊引用時),避免解析和重定向文本之外,還有避免版本不同導致項目出現意外情況。

使用終端命令可以快速簡單下載代碼到本地:

curl unpkg.com/petite-vue@0.4.1/dist/petite-vue.iife.js --output petite-vue@0.4.1-iife.js

根作用域 (Root Scope) / 作用域 (Scope)

這裡的作用域和我們編寫 JavaScript 時說的作用域是一致的,作用是限定函數和變量的可用範圍,減少命名衝突。具有如下特點:

  1. 作用域之間存在父子關係和兄弟關係,整體構成一顆作用域樹。
  2. 子作用域的變量或屬性可覆蓋祖先作用域同名變量或屬性的訪問性。
  3. 若對僅祖先作用域存在的變量或屬性賦值,將賦值給祖先作用域的變量或屬性。
// 全局作用域
var globalVariable = 'hello'
var message1 = 'there'
var message2 = 'bye'

(() => {
  // 局部作用域 A
  let message1 = '局部作用域A'
  message2 = 'see you'
  console.log(globalVariable, message1, message2)
})()
// 輸出:hello 局部作用域 A see you

(() => {
  // 局部作用域 B
  console.log(globalVariable, message1, message2)
})()
// 輸出:hello there see you

createApp(),它接收一個數據對像作為根範圍中的變量,供模版中使用。

<script type="module">
  import { createApp } from 'https://unpkg.com/petite-vue?module';

  createApp({
    count: 0,
    // getters
    get plusOne() {
      return this.count + 1;
    },
    increment() {
      this.count++;
    },
  }).mount();
</script>

<!-- v-scope value can be omitted -->
<div v-scope>
  <p>{{ count }}</p>
  <p>{{ plusOne }}</p>
  <button @click="increment">increment</button>
</div>

這樣在模版中就可以使用 count 變量、increment 方法了。

掛載元素 (Mount)

mount() 沒有傳入掛載元素時,Petite-Vue 作用於整個頁面,但當傳入掛載元素時,那麼僅作用於掛載元素及其內的元素。

這也意味著,我們可以在同一頁面掛載多個 Petite-Vue 應用,每一個應用都有其獨立的根變量範圍。

createApp({
  // root scope for app one
}).mount('#app1');

createApp({
  // root scope for app two
}).mount('#app2');

生命週期

在 Petite-Vue 中,可以監聽每一個元素的掛載和卸載事件。

在 v0.4.0 開始,綁定生命週期事件需要加上 @vue: 前綴。

<!-- v0.4.0 以下 -->
<div v-if="show" @mounted="console.log('mounted on: ', $el)" @unmounted="console.log('unmounted: ', $el)">
  some node
</div>

<!-- v0.4.0 以上 -->
<div v-if="show" @vue:mounted="console.log('mounted on: ', $el)" @vue:unmounted="console.log('unmounted: ', $el)">
  some node
</div>

v-effect

v-effect 執行響應式內聯語句:

<div v-scope="{ count: 0 }">
  <div v-effect="$el.textContent = count"></div>
  <button @click="count++">++</button>
</div>

effect 響應式的追蹤其依賴,並在依賴更改時重新執行,因此每當更改 count 時它都會重新運行。

組件 (Component)

組件的概念在 Petite-Vue 中有所不同,因為它更加簡單。

組件有兩種方式來創建,分別是純數據的函數組件帶有模板的函數組件

使用組件,是需要在元素中用 v-scope 來調用函數。

純數據的函數組件

<script type="module">
  import { createApp } from 'https://unpkg.com/petite-vue?module'

  function Counter(props) {
    return {
      count: props.initialCount,
      inc() {
        this.count++
      },
      mounted() {
        console.log(`I'm mounted!`)
      }
    }
  }

  createApp({
    Counter
  }).mount()
</script>

<div v-scope="Counter({ initialCount: 1 })" @vue:mounted="mounted">
  <p>{{ count }}</p>
  <button @click="inc">increment</button>
</div>

<div v-scope="Counter({ initialCount: 2 })">
  <p>{{ count }}</p>
  <button @click="inc">increment</button>
</div>

帶有模板的函數組件

如果還需要模板的話,相比函數組件是多了一個字段來聲明模板:$template,該字段的值可以是一個模板字符串,也可以是 <template> 元素的 ID 選擇器。(推薦用 template 元素)。

<script type="module">
  import { createApp } from 'https://unpkg.com/petite-vue?module';

  function Counter(props) {
    return {
      $template: '#counter-template',
      // $template: `
      //         My count is {{count}}
      //         <button @click="inc">+aaa+</button>
      //         `,
      count: props.initialCount,
      inc() {
        this.count++;
      },
    };
  }

  createApp({
    Counter,
  }).mount();
</script>

<template id="counter-template">
  My count is {{ count }}
  <button @click="inc">++</button>
</template>

<!-- reuse it -->
<div v-scope="Counter({ initialCount: 1 })"></div>
<div v-scope="Counter({ initialCount: 2 })"></div>

全局狀態管理

沒錯,即便是簡單的 mini Vue,也可以有全局狀態管理。這裡使用的是 vue3 的 reactive API,來實現全局狀態管理。

<script type="module">
  import { createApp, reactive } from 'https://unpkg.com/petite-vue?module'

  const store = reactive({
    count: 0,
    inc() {
      this.count++
    }
  })

  // manipulate it here
  store.inc()

  createApp({
    // share it with app scopes
    store
  }).mount()
</script>

<div v-scope="{ localCount: 0 }">
  <p>Global {{ store.count }}</p>
  <button @click="store.inc">increment</button>

  <p>Local {{ localCount }}</p>
  <button @click="localCount++">increment</button>
</div>

指令 (Directive)

內建指令

  • v-model
  • v-if / v-else / v-else-if
  • v-for
  • v-show
  • v-on(别名:@)
  • v-bind(别名::)
  • v-html / v-text / v-pre
  • v-once
  • v-cloak (可用來配合 CSS 做未渲染時隱藏)

自定義指令

Petite-Vue 的自定義指令與 vue 有些不同,那麼怎麼註冊一個指令?

  • 指令聲明:一個函數,const myDirective = (ctx) => {};
    • ctx 是一個對象,裡面有 elarggeteffect 等屬性,具體可以參考文檔或者閱讀源碼
    • 函數 return,是在指令卸載時候會觸發。
  • 註冊指令:createApp().directive('dir-name', dirFn).mount()

v-html 指令的實現:

const html = ({ el, get, effect }) => {
  // effect 每次 get() 更改後就會執行
  effect(() => {
    el.innerHTML = get();
  });
};

自定義指令範例:

const myDirective = (ctx) => {
  // the element the directive is on
  ctx.el
  // the raw value expression
  // e.g. v-my-dir="x" then this would be "x"
  ctx.exp
  // v-my-dir:foo -> "foo"
  ctx.arg
  // v-my-dir.mod -> { mod: true }
  ctx.modifiers
  // evaluate the expression and get its value
  ctx.get()
  // evaluate arbitrary expression in current scope
  ctx.get(`${ctx.exp} + 10`)

  // run reactive effect
  ctx.effect(() => {
    // this will re-run every time the get() value changes
    console.log(ctx.get())
  })

  return () => {
    // cleanup if the element is unmounted
  }
}

// register the directive
createApp().directive('my-dir', myDirective).mount()

自定義模板

這個可以通過給 createApp 的配置項增加屬性 $delimiters: ['${', '}'] 來實現,通常在服務器端模板語言一起使用時比較有用。

Petite-Vue vs Vue

Petite-Vue 僅有的特性:

  • v-scope
  • v-effect
  • @vue:mounted、@vue:unmounted 事件

不同的功能:

  1. 在表達式中,$el 指向指令綁定到的當前元素(而不是組件根元素)
  2. createApp() 接受全局狀態而不是組件
  3. 組件被簡化為函數返回對象
  4. 自定義指令有不同的語法

共用的特性:

共有的特性
{{ }}
v-bind
v-on
v-model
v-if / v-else / v-else-if
v-for
v-show
v-html
v-text
v-pre
v-once
v-cloak
reactive()
nextTick()
Template refs

Petite-Vue 不支持的特性:

一些特性因為它們在漸進增強的背景下具有相對較低的使用頻率而被丟棄。

如果你需要這些特性,你可能應該只使用標準的 Vue:

  1. ref()computed() 等組合式API
  2. Render functions ( Petite-Vue 沒有 Virtual DOM)
  3. Reactivity 響應式數據類型(Map、Set、...等,為了體積更小而刪除)
  4. TransitionKeepAliveTeleportSuspense
  5. v-for 的深解構
  6. v-on="object" 對象語法
  7. v-is 動態組件
  8. v-bind:style 自動前綴

總結

總結來說,Petite-Vue 這個項目,挺適合「用不到 Vue、React 等這些偏大的前端框架的簡單頁面」,或是「既有的非前後端分離的歷史項目」來做使用的,有著 Vue 的開發體驗,也能不增加項目的複雜度,簡直是一個完美的體驗!


作者:Wayne (偉恩)
連結:https://wayne-blog.com/
來源:Wayne's blog | 偉恩的部落格 | 技術博客



圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言